package com.echat.api.demo.serviceapi.betterCode;

import com.alibaba.fastjson.JSON;
import okhttp3.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.ConnectException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
 * Created by chenmohan on 2018/09/13
 *
 * <p>封装了关于accessToken的常用Http请求,可以直接拿来使用。</p>
 * <p>单机环境下,创建出对象后,直接调用start()方法就可以自动刷新accessToken；</p>
 * <p>集群或分布式环境，需要根据提供的方法做一些额外开发。</p>
 */
public class EchatTokenManager {

    private Logger log = Logger.getLogger(EchatTokenManager.class.getName());

    private String host = null;

    private String accessKey = null;

    private String accessSecret = null;

    /**
     * 【单位:秒】token剩余有效时间的秒数，默认为12分钟
     */
    private long tokenExpireThreshhold = 12 * 60;

    /**
     * 【单位:秒】定时请求token的ttl , 调度周期默认为5分钟
     */
    private long scheduleCycle = 5 * 60;

    /**
     * 回调接口，用于功能扩展，详细见接口说明
     */
    private TokenNotify tokenNotify = new DefaultTokenNotify();

    private static OkHttpClient client = new OkHttpClient();

    private String token = null;

    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    /**
     * 获取当前token
     *
     * @return
     */
    public String token() {
        return token;
    }

    /**
     * 开始 判断ttl是否即将过期 -> 重新获取token 的定时调度任务
     * 单机环境可以直接使用此方法进行任务定时调度
     */
    public void start() throws RuntimeException {

        scheduler.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                try {
                    if (token == null) {
                        accquireToken();
                        return;
                    }
                    long ttl = ttl(token);
                    if (ttl <= tokenExpireThreshhold) {
                        accquireToken();
                    }
                } catch (Exception e) {
                    log.warning("some error occurs , the scheduler is shutdowning");
                    log.warning(getErrorInfoFromException(e));
                    scheduler.shutdown();
                }
            }
        }, 0, scheduleCycle, TimeUnit.SECONDS);

    }

    /**
     * 发送Http请求获取accessToken
     *
     * @return 访问令牌
     */
    public String accquireToken() {
        FormBody formBody = new FormBody.Builder()
                .add("accessKey", accessKey)
                .add("accessSecret", accessSecret)
                .build();
        Request getTokenRequest = new Request.Builder().post(formBody).url(getTokenUrl()).build();
        Response tokenResponse = execute(client.newCall(getTokenRequest));
        String result = responseToString(tokenResponse.body());
        tokenResponse.body().close();
        String accessToken = (String) JSON.parseObject(result).get("accessToken");
        Integer code = (Integer) JSON.parseObject(result).get("code");
        if (accessToken == null && code == null) {
            throw new RuntimeException("accquire accessToken is null , please check your profile!");
        }
        if (code != null) {
            String msg = (String) JSON.parseObject(result).get("msg");
            throw new RuntimeException("accquire failed , errorCode = " + code.intValue() + " , errorMsg = '" + msg + "'");
        }
        token = accessToken;
        tokenNotify.notify(token);  //token成功获取到后，调用回调接口
        log.info("{" + Thread.currentThread().getName() + "}accquire accessToken[" + token + "] success!");
        return accessToken;
    }

    /**
     * <p>发送Http请求获取指定accessToken的剩余有效时间</p>
     * <p>小于0表示该accessToken已过期</p>
     *
     * @param accessToken 访问令牌
     * @return token剩余有效时间(单位 : 秒)
     */
    public long ttl(String accessToken) {
        Request getTTLRequest = new Request.Builder()
                .url(getTTLUrl())
                .get().build();
        Response ttlResponse = execute(client.newCall(getTTLRequest));
        String result = responseToString(ttlResponse.body());
        return Long.valueOf(result);
    }

    /**
     * 在accessToken获取成功后立即会调用回调接口,分布式环境下可以将此acessToken存放到别的平台上
     */
    public interface TokenNotify {
        void notify(String accessToken);
    }

    /**
     * accessToken刷新回调接口的默认空实现
     */
    private class DefaultTokenNotify implements TokenNotify {
        public void notify(String accessToken) {
            return;
        }
    }

    /**
     * 拼接成获取token的请求地址
     *
     * @return 获取token的请求地址
     */
    private String getTokenUrl() {
        return new String(host + "/auth/accquire").intern();
    }

    /**
     * 拼接成获取accessToken剩余有效时间的请求地址
     *
     * @return 获取accessToken剩余有效时间的请求地址
     */
    private String getTTLUrl() {
        return new String(host + "/auth/ttl").intern();
    }

    /**
     * 用trycatch包裹call.execute();
     */
    private Response execute(Call call) {
        try {
            return call.execute();
        } catch (ConnectException e) {
            throw new RuntimeException("Failed to connect to " + host + " ,please check the profile or call developer");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 用trycatch包裹return body.string();
     */
    private String responseToString(ResponseBody body) {
        try {
            return body.string();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public EchatTokenManager(String host, String accessKey, String accessSecret) {
        this.host = host;
        this.accessKey = accessKey;
        this.accessSecret = accessSecret;
        checkNotNull();
    }

    public EchatTokenManager(String host, String accessKey, String accessSecret, TokenNotify tokenNotify) {
        this(host, accessKey, accessSecret);
        this.tokenNotify = tokenNotify;
        checkNotNull();
    }

    public EchatTokenManager(String host, String accessKey, String accessSecret, long tokenExpireThreshhold, long scheduleCycle) {
        this.host = host;
        this.accessKey = accessKey;
        this.accessSecret = accessSecret;
        this.tokenExpireThreshhold = tokenExpireThreshhold;
        this.scheduleCycle = scheduleCycle;
        checkNotNull();
    }

    public EchatTokenManager(String host, String accessKey, String accessSecret, long tokenExpireThreshhold, long scheduleCycle, TokenNotify tokenNotify) {
        this(host, accessKey, accessSecret, tokenExpireThreshhold, scheduleCycle);
        this.tokenNotify = tokenNotify;
        checkNotNull();
    }

    private void checkNotNull() {
        if (host == null || host.trim().equals("")) {
            throw new IllegalArgumentException("'host' must not be null or empty string");
        }
        if (accessKey == null || accessKey.trim().equals("")) {
            throw new IllegalArgumentException("'accessKey' must not be null or empty string");
        }
        if (accessSecret == null || accessSecret.trim().equals("")) {
            throw new IllegalArgumentException("'accessSecret' must not be null or empty string");
        }
        if (tokenNotify == null) {
            throw new IllegalArgumentException("'tokenNotify' must not be null");
        }
    }

    /**
     * 必须使用带参构造方法创建对象, Spring xml请使用<constructor-arg>注入属性
     *
     * @throws IllegalAccessException
     */
    private EchatTokenManager() throws IllegalAccessException {
        throw new IllegalAccessException("please use ArgsConstructor of EchatTokenManager");
    }

    /**
     * 将堆栈信息包装成String,方便打印日志
     */
    private static String getErrorInfoFromException(Exception e) {
        try {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            return "\r\n" + sw.toString() + "\r\n";
        } catch (Exception e2) {
            return "bad getErrorInfoFromException";
        }
    }

}
